﻿/*
 * fbasync - An Asynchronous Facebook API for .NET
 * Copyright (C) 2007 ittybittysoft
 * Email: info@ittybittysoft.com
 * Portions of the library are based on the Facebook Developer Toolkit
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

using System;
using System.Configuration;
using System.Security.Principal;
using System.Web;
using System.Web.Security;
using System.Collections.Generic;

namespace fbasync.webcomponents
{
    public class ApplicationKeyEventArgs
        : EventArgs
    {
        public string ApplicationKey { get; set; }
        public string Secret { get; set; }
    }

    public class FacebookAuthenticatedEventArgs
        : EventArgs
    {
        public FacebookConnection Connection { get; set; }
        public bool NewSession { get; set; }
        public bool? IsInCanvas { get; set; }
        public bool? IsAppAdded { get; set; }
        public bool? IsInIFrame { get; set; }
        public bool? IsConnect { get; set; }
        public bool Authenticated { get; set; }

        internal HttpContext Context { get; set; }
        internal FormsAuthenticationTicket OldAuthTicket { get; set; }
        internal string RedirectUrl { get; set; }
    }

    public class FacebookAuthenticationModule
        : IHttpModule
    {
        public event EventHandler<FacebookAuthenticatedEventArgs> Authenticated;
        public event EventHandler<ApplicationKeyEventArgs> ApplicationKeyNeeded;

        private class AnonymousIdentity : IIdentity
        {
            #region IIdentity Members

            public string AuthenticationType
            {
                get { return "anonymous"; }
            }

            public bool IsAuthenticated
            {
                get { return false; }
            }

            public string Name
            {
                get { return string.Empty; }
            }

            #endregion
        }


        public void Dispose()
        {
        }

        public void Init(HttpApplication context)
        {
            //this is to make our cookies work in an iframe in IE6
            context.PreSendRequestHeaders += delegate
            {
                context.Context.Response.AppendHeader("P3P", "CP=\"CAO PSA OUR\"");
            };

            context.AddOnAuthenticateRequestAsync(BeginAuthenticateRequest, EndAuthenticateRequest);
        }

        public string CookieName { get; set; }
        public int AuthTimeout { get; set; }
        public FacebookConnection ConnectionTemplate { get; set; }
        public bool AutoRedirectToLoginPage { get; set; }
        public bool UseAmbientSessionOnly { get; set; }
        private object _initSync = new object();

        public static void RedirectToLoginPage(HttpContext ctx, string applicationKey, bool endRequest)
        {
            var response = ctx.Response;
            var url = FacebookConnection.GetLoginRedirectUrl(applicationKey, "?fbasyncRedirectUrl=" + ctx.Request.Url.ToString());
            response.BufferOutput = true;
            response.Cache.SetCacheability(HttpCacheability.NoCache);
            bool inIframe = ctx.Request.Params["fb_sig_in_iframe"] == "1";
            bool inCanvas = ctx.Request.Params["fb_sig_in_canvas"] == "1";

            if (inIframe)
                response.Write("<script type=\"text/javascript\">\nif (parent != self) \ntop.location.href = \"" + url + "\";\nelse self.location.href = \"" + url + "\";\n</script>");
            else if (inCanvas)
                response.Write("<fb:redirect url=\"" + url + "\" />");

            if (endRequest)
                ctx.ApplicationInstance.CompleteRequest();
        }

        protected IAsyncResult BeginAuthenticateRequest(Object sender, EventArgs e, AsyncCallback cb, object state)
        {
            HttpContext ctx = HttpContext.Current;

            InitializeSettings();

            LazyAsyncResult<FacebookAuthenticatedEventArgs> myAr = new LazyAsyncResult<FacebookAuthenticatedEventArgs>(new FacebookAuthenticatedEventArgs(), cb, state);
            myAr.MyAsyncState.Context = ctx;
            myAr.MyAsyncState.RedirectUrl = ctx.Request.Params["fbasyncRedirectUrl"];
            var authCookie = ctx.Request.Cookies[CookieName];
            FormsAuthenticationTicket ticket = null;

            //read current cookie to determine if we have a new session or not
            if (null != authCookie && !UseAmbientSessionOnly)
            {
                ticket = FormsAuthentication.Decrypt(authCookie.Value);
                if (ticket.Expired)
                {
                    ticket = null;
                    myAr.MyAsyncState.NewSession = true;
                }
                else
                {
                    myAr.MyAsyncState.NewSession = false;
                    myAr.MyAsyncState.OldAuthTicket = ticket;
                }
            }
            else myAr.MyAsyncState.NewSession = true;

            bool inCanvas = ctx.Request.Params["fb_sig_in_canvas"] == "1";
            bool isConnect = null != ctx.Request.Cookies[ConnectionTemplate.ApplicationKey + "_session_key"];
            myAr.MyAsyncState.IsInCanvas = inCanvas;
            myAr.MyAsyncState.IsConnect = isConnect;

            string authToken = ctx.Request.Params["auth_token"];

            var ambientSession = ctx.GetAmbientSession(ConnectionTemplate);
            if (null != ambientSession && ambientSession.IsSessionActive())
            {
                //got a session from facebook provided parameters
                myAr.MyAsyncState.Connection = ambientSession;
                myAr.MyAsyncState.Authenticated = true;
                myAr.MyAsyncState.IsAppAdded = ctx.Request.Params["fb_sig_added"] == "1";
                myAr.MyAsyncState.IsInCanvas = ctx.Request.Params["fb_sig_in_canvas"] == "1";
                myAr.MyAsyncState.IsInIFrame = ctx.Request.Params["fb_sig_in_iframe"] == "1";
                myAr.SetComplete(true);
                return myAr;
            }
            else if (null != ticket && !UseAmbientSessionOnly)
            {
                //if we didn't get a session from facebook, use the one in the ticket
                myAr.MyAsyncState.Authenticated = true;
                myAr.MyAsyncState.Connection = ticket.DeserializeConnection(ConnectionTemplate.Secret);

                myAr.SetComplete(true);
                return myAr;
            }
            else if (string.IsNullOrEmpty(authToken) && !UseAmbientSessionOnly && AutoRedirectToLoginPage)
            {
                //no auth token provided. redirect to login page right now and remember our redirect url.
                RedirectToLoginPage(ctx, ConnectionTemplate.ApplicationKey, true);
                myAr.SetComplete(true);
                return myAr;
            }
            else if (string.IsNullOrEmpty(authToken) && !UseAmbientSessionOnly && !AutoRedirectToLoginPage)
            {
                //this app is set to not redirect, which means we don't do anything at this level.
                //authentication is handled by another component.
                //we'll end up with an ambient session once they authenticate on the client side.
                myAr.SetComplete(true);
                return myAr;
            }
            else if (!string.IsNullOrEmpty(authToken) && !UseAmbientSessionOnly)
            {
                //we have an auth token. use it to get a new session.
                //this bit is the only reason this component is asynchronous.
                myAr.MyAsyncState.Connection = ConnectionTemplate.Clone(true, true);
                myAr.MyAsyncState.Connection.BeginGetSession(authToken,
                    (ar) =>
                    {
                        try
                        {
                            myAr.MyAsyncState.Connection.EndGetSession(ar);
                            myAr.MyAsyncState.NewSession = true;
                            myAr.MyAsyncState.Authenticated = true;

                            if (!myAr.IsCompleted)
                                myAr.SetComplete();
                        }
                        catch (Exception ex)
                        {
                            if (!myAr.IsCompleted)
                                myAr.SetComplete(ex);
                        }
                    },
                    null);

                return myAr;
            }
            else
            {
                //we are anonymous and the user didn't want us to redirect to login page.
                //so anonymous we stay!
                myAr.SetComplete(true);
                return myAr;
            }
        }

        private void InitializeSettings()
        {
            //the first time through we check for our connection template.
            //the application can then provide us with the api key and secret they want to use.
            //this is done here so we can raise an event the application can handle in thir HttpApplication instance.
            if (null == ConnectionTemplate)
            {
                lock (_initSync)
                {
                    if (null == ConnectionTemplate)
                    {
                        ConnectionTemplate = new FacebookConnection();
                        ApplicationKeyEventArgs ae = new ApplicationKeyEventArgs { ApplicationKey = ConnectionTemplate.ApplicationKey, Secret = ConnectionTemplate.Secret };
                        OnApplicationKeyNeeded(ae);
                        ConnectionTemplate.ApplicationKey = ae.ApplicationKey;
                        ConnectionTemplate.Secret = ae.Secret;

                        if (string.IsNullOrEmpty(ConnectionTemplate.ApplicationKey) || string.IsNullOrEmpty(ConnectionTemplate.Secret))
                            throw new InvalidOperationException("Application credentials are needed. Either provide them in the web.config or through the ApplicationKeyNeeded event.");

                        CookieName = ConfigurationManager.AppSettings.Get("fbasync.auth.cookie") ?? ".fbasyncauth" + "_" + ConnectionTemplate.ApplicationKey;
                        AuthTimeout = int.Parse(ConfigurationManager.AppSettings.Get("fbasync.auth.timeout") ?? "20");
                        AutoRedirectToLoginPage = bool.Parse(ConfigurationManager.AppSettings.Get("fbasync.auth.redirecttologin") ?? "true");
                        UseAmbientSessionOnly = bool.Parse(ConfigurationManager.AppSettings.Get("fbasync.auth.ambientsessiononly") ?? "false");
                    }
                }
            }
        }

        protected void EndAuthenticateRequest(IAsyncResult ar)
        {
            LazyAsyncResult<FacebookAuthenticatedEventArgs> myAr = (LazyAsyncResult<FacebookAuthenticatedEventArgs>)ar;
            var state = myAr.End<FacebookAuthenticatedEventArgs>();

            if (state.Authenticated)
            {
                //TODO: potentially provide some method other than cookies for authentication persistance
                //only persist cookies for out-of-canvas requests.
                //also only write our cookie if we're not exclusively using the ambient session.
                if (!myAr.MyAsyncState.IsInCanvas.GetValueOrDefault(false) && !UseAmbientSessionOnly)
                {
                    var ticket = state.Connection.ToAuthTicket(state.OldAuthTicket, AuthTimeout);
                    string encryptedTicket = FormsAuthentication.Encrypt(ticket);
                    var authCookie = new HttpCookie(CookieName, encryptedTicket);
                    authCookie.Expires = ticket.Expiration;
                    state.Context.Response.AppendCookie(authCookie);
                }

                state.Context.SetUserConnection(state.Connection);
                OnAuthenticated(state);

                //make sure the request is marked as authenticated
                if (null == state.Context.User)
                {
                    GenericIdentity id = new GenericIdentity(state.Connection.UserId.Value.ToString(), "fbasync");
                    state.Context.User = new GenericPrincipal(id, new string[] { });
                }
            }

            if (!string.IsNullOrEmpty(state.RedirectUrl))
                state.Context.Response.Redirect(state.RedirectUrl, true);
        }

        protected virtual void OnAuthenticated(FacebookAuthenticatedEventArgs e)
        {
            if (null != Authenticated)
                Authenticated(this, e);
        }

        protected virtual void OnApplicationKeyNeeded(ApplicationKeyEventArgs e)
        {
            if (null != ApplicationKeyNeeded)
                ApplicationKeyNeeded(this, e);
        }

    }
}
